package org.mullin.stratego.server;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.mullin.stratego.client.StrategoService;
import org.mullin.stratego.shared.Match;
import org.mullin.stratego.shared.Player;
import org.mullin.stratego.shared.RandomState;
import org.mullin.stratego.shared.State;

import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import static com.googlecode.objectify.ObjectifyService.ofy;

public class StrategoServiceImpl extends RemoteServiceServlet implements StrategoService {

	public static final long serialVersionUID = 1L;
	
	// map from playerId to playerId - key = player, val = opponent.
	private Map<String, String> playerToOpponent;
	// our channel service.
	private ChannelService channelService;
	// starting state generator
	private RandomState stateGenerator;
	
	private List<String> waitingPlayers;
	
	private Random rand;
	
	public StrategoServiceImpl() {
		super();
		playerToOpponent = new HashMap<String, String>();
		channelService = ChannelServiceFactory.getChannelService();
		
		stateGenerator = new RandomState();
		
		waitingPlayers = new ArrayList<String>();
		rand = new Random();
	}	
	
	/**
	 * Register a player.
	 * 
	 * @param channelId player entering game.
	 */
	@Override
	public void enterGame(String id) {
		System.out.println("enterGame, adding player=" + id);
		
		Player player = ofy().load().type(Player.class).id(id).now();
		if (null == player) {
			System.out.println("player not found in store, new player.");
			player = new Player(id);
		}
		ofy().save().entity(player).now();
		
		if (!waitingPlayers.contains(id)) {
			System.out.println("enterGame, was not in the waiting list.");
			waitingPlayers.add(id);
		}
	}
	
	// CONSIDER RETURNING THE STRING TO CLIENT AND SENDING STRING TO OPPONENT
	// INSTEAD OF RETURNING VOID AND SENDING TO BOTH
	@Override
	public void autoMatch(String id) {
		System.out.println("autoMatch, id=" + id);
		
		for (String waiting : waitingPlayers) {
			if (!id.equals(waiting)) {
				System.out.println("autoMatch, found opponent=" + waiting);
				waitingPlayers.remove(waiting);
				waitingPlayers.remove(id);
				newMatch(id, waiting);
				return;
			}
		}
	}
	
	/**
	 * Start a match between two players. Player 1 requested the match with a specific opponent.
	 * 
	 * @param p1 Player 1.
	 * @param requestedOpponent Opponent requested by player 1.
	 */
	public void newMatchWithEmail(String p1, String requestedOpponent) {
		newMatch(p1, requestedOpponent);
	}
	
	/**
	 * Update player + opponent map, generate starting state, and create the match.
	 * 
	 * @param p1 id of player 1.
	 * @param p2 id of player 2.
	 */
	private void newMatch(String p1, String p2) {
		long matchId = rand.nextLong();
		
		// p1 -> p2, p2 -> p1
		playerToOpponent.put(p1 + matchId, p2 + matchId);
		playerToOpponent.put(p2 + matchId, p1 + matchId);
		
		State s = stateGenerator.generateRandomStartingState();
		String m = State.serialize(s);
		
		Player red = ofy().load().type(Player.class).id(p1).now();
		Player blue = ofy().load().type(Player.class).id(p2).now();
		
		// S-ID-COLOR-STATE
		send(p1, "S&" + Long.toString(matchId) + "&R&" + m + "&" + red.getRank().getRating());
		send(p2, "S&" + Long.toString(matchId) + "&B&" + m + "&" + blue.getRank().getRating());
		storeNewMatch(p1, p2, matchId, m);
	}
	
	/**
	 * Create and store a new match. Add match to each players history.
	 * 
	 * @param redPlayer
	 * @param bluePlayer
	 * @param matchId
	 */
	private void storeNewMatch(String redPlayer, String bluePlayer, long matchId, String startState) {
		System.out.println("storeNewMatch, redPlayer=" + redPlayer + ", bluePlayer=" + bluePlayer + " matchId=" + matchId);
		
		Match match = new Match(redPlayer, bluePlayer, matchId);
		match.setSerializedState(startState);
		match.setStartDate(new Date());
		
		Player red = ofy().load().type(Player.class).id(redPlayer).now();
		Player blue = ofy().load().type(Player.class).id(bluePlayer).now();
		
		if (null == red) {
			System.out.println("storeNewMatch, red was null");
			red = new Player(redPlayer, 0);
		}
		if (null == blue) {
			System.out.println("storeNewMatch, blue was null");
			blue = new Player(bluePlayer, 0);
		}
		
		System.out.println("adding match to players");
		
		red.addMatch(matchId);
		blue.addMatch(matchId);
		
		ofy().save().entity(match).now();
		ofy().save().entity(red).now();
		ofy().save().entity(blue).now();
	}
	
	/**
	 * Send a message over a channel.
	 * 
	 * @param channel
	 * @param msg
	 */
	private void send(String channel, String msg) {
		channelService.sendMessage(new ChannelMessage(channel, msg));
	}
	
	
	/**
	 * Send a move from a player to his opponent.
	 * 
	 * @param m serialized move.
	 * @param channelId player making move.
	 */
	@Override
	public void sendMove(String move, String id, long matchId) {
		String opponent = playerToOpponent.get(id + matchId);
		System.out.println("Sending move from " + id + " in match " + matchId + " to " + opponent + ", move=" + move);
		if (null != opponent) {
			opponent = opponent.replace(Long.toString(matchId), "");
			send(opponent, "M" + move);
			storeMove(matchId, move);
		}
	}
	
	/**
	 * Store a move in a matches history.
	 * 
	 * @param matchId Match to update.
	 * @param move Move to save.
	 */
	private void storeMove(long matchId, String move) {
		Match match = ofy().load().type(Match.class).id(matchId).now();
		match.addMove(move);
		ofy().save().entity(match).now();
	}
	
	
	/**
	 * Get all matches for a given player.
	 * 
	 * @param id id of player whose matches to fetch.
	 */
	@Override
	public Collection<Match> getMatches(String id) {
		Player p = ofy().load().type(Player.class).id(id).now();
		
		if (null == p) {
			System.out.println("getMatches- player was null");
			return new ArrayList<Match>();
		}
		
		Map<Long, Match> matches = ofy().load().type(Match.class).ids(p.matches);
		
		return new ArrayList<Match>(matches.values());
	}
	
	/**
	 * Get a specific match with id of matchId.
	 * 
	 * @param matchId id of match to fetch.
	 */
	@Override
	public Match getMatch(long matchId) {
		Match m = ofy().load().type(Match.class).id(matchId).now();
		
		String redPlayer = m.getRedPlayer();
		String bluePlayer = m.getBluePlayer();
		
		if (!playerToOpponent.containsKey(redPlayer + matchId)) {
			System.out.println("loaded match, but playerToOpponent did not conatin opponent info for red- adding.");
			playerToOpponent.put(redPlayer + matchId, bluePlayer + matchId);
		}
		
		if (!playerToOpponent.containsKey(bluePlayer + matchId)) {
			System.out.println("loaded match, but playerToOpponent did not conatin opponent info for blue- adding.");
			playerToOpponent.put(bluePlayer + matchId, redPlayer + matchId);
		}
		
		return m;
	}
	
	/**
	 * Delete a match from a players history.
	 * 
	 * @param matchId match to remove
	 * @param player player to update
	 */
	@Override
	public void deleteMatch(long matchId, String player) {
		Match match = ofy().load().type(Match.class).id(matchId).now();
		Player redPlayer = ofy().load().type(Player.class).id(match.getRedPlayer()).now();
		Player bluePlayer = ofy().load().type(Player.class).id(match.getBluePlayer()).now();
		
		if (player.equals(redPlayer.email)) {
			redPlayer.removeMatch(matchId);
			
			redPlayer.setLastGameDate(new Date());
			
			if (!bluePlayer.getMatches().contains(matchId)) {
				ofy().delete().entity(match);
			}
			
		} else {
			bluePlayer.removeMatch(matchId);
			
			bluePlayer.setLastGameDate(new Date());
			
			if (!redPlayer.getMatches().contains(matchId)) {
				ofy().delete().entity(match);
			}
		}
		
		ofy().save().entity(redPlayer).now();
		ofy().save().entity(bluePlayer).now();
	}
	
}
